package com.cl.code.database.mybatis.module.sql;


import com.cl.code.database.mybatis.constant.CLMybatisConstants;
import com.cl.code.database.mybatis.module.sql.condition.CombinationConditionItem;
import com.cl.code.database.mybatis.module.sql.condition.ConditionItem;
import com.cl.code.database.mybatis.module.sql.condition.ParamValue;
import com.cl.code.database.mybatis.module.sql.condition.SingleConditionItem;
import com.cl.code.database.mybatis.module.sql.operator.*;
import lombok.Getter;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * 基础 Search
 *
 * @author chengliang
 * @since 1.0.0
 */
public class Where<Entity> {

    private final Class<Entity> entityClass;

    private final Stack<LogicSqlOperator> andOrStack = new Stack<>();

    @Getter
    private final Map<String, Object> map;

    private final AtomicInteger paramCount;

    @Getter
    private final List<ConditionItem> conditions = new ArrayList<>(5);

    public Class<Entity> getEntityClass() {
        return this.entityClass;
    }

    private Where(Class<Entity> entityClass, Map<String, Object> map, AtomicInteger paramCount) {
        this.entityClass = entityClass;
        this.map = map;
        this.paramCount = paramCount;
    }

    public static <Entity> Where<Entity> of(Class<Entity> tClass) {
        return new Where<>(tClass, new HashMap<>(16), new AtomicInteger(0));
    }

    public Split<Entity> equalTo(Fn<Entity, Object> column, Object value) {
        checkColumn(column);
        if (value == null) {
            return new Split<>(this);
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), CompareSqlExpression.EQUAL, handleParamValue(value)));
    }

    public Split<Entity> equalTo(String column, Object value) {
        if (value == null) {
            return new Split<>(this);
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column, CompareSqlExpression.EQUAL, handleParamValue(value)));
    }

    public Split<Entity> notEqualTo(Fn<Entity, Object> column, Object value) {
        checkColumn(column);
        if (value == null) {
            return new Split<>(this);
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), CompareSqlExpression.NOT_EQUAL, handleParamValue(value)));
    }

    public Split<Entity> greaterThan(Fn<Entity, Object> column, Object value) {
        checkColumn(column);
        if (value == null) {
            return new Split<>(this);
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), CompareSqlExpression.GREATER_THAN, handleParamValue(value)));
    }

    public Split<Entity> greaterThanOrEqualTo(Fn<Entity, Object> column, Object value) {
        checkColumn(column);
        if (value == null) {
            return new Split<>(this);
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), CompareSqlExpression.GREATER_THAN_OR_EQUAL, handleParamValue(value)));
    }

    public Split<Entity> lessThan(Fn<Entity, Object> column, Object value) {
        checkColumn(column);
        if (value == null) {
            return new Split<>(this);
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), CompareSqlExpression.LESS_THAN, handleParamValue(value)));
    }

    public Split<Entity> lessThanOrEqualTo(Fn<Entity, Object> column, Object value) {
        checkColumn(column);
        if (value == null) {
            return new Split<>(this);
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), CompareSqlExpression.LESS_THAN_OR_EQUAL, handleParamValue(value)));
    }

    public Split<Entity> like(Fn<Entity, Object> column, String value) {
        checkColumn(column);
        if (value == null || value.isEmpty()) {
            return new Split<>(this);
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), FuzzySqlExpression.LIKE, handleParamValue(value)));
    }

    public Split<Entity> notLike(Fn<Entity, Object> column, String value) {
        checkColumn(column);
        if (value == null || value.isEmpty()) {
            return new Split<>(this);
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), FuzzySqlExpression.NOT_LIKE, handleParamValue(value)));
    }

    public Split<Entity> startWith(Fn<Entity, Object> column, String value) {
        checkColumn(column);
        if (value == null || value.isEmpty()) {
            return new Split<>(this);
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), FuzzySqlExpression.START_WITH, handleParamValue(value)));
    }

    public Split<Entity> endWith(Fn<Entity, Object> column, String value) {
        checkColumn(column);
        if (value == null || value.isEmpty()) {
            return new Split<>(this);
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), FuzzySqlExpression.END_WITH, handleParamValue(value)));
    }

    public Split<Entity> in(Fn<Entity, Object> column, Collection<?> values) {
        return in(column.getName(), values);
    }

    public Split<Entity> in(String column, Collection<?> values) {
        if (values == null || values.isEmpty()) {
            return new Split<>(this);
        }
        if (values.stream().anyMatch(Objects::isNull)) {
            throw new IllegalArgumentException("values cannot contain null");
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column, CollectionSqlExpression.IN, handleParamValue(values.toArray())));
    }

    public Split<Entity> in(Fn<Entity, Object> column, Object... values) {
        List<Object> collect = Arrays.stream(values).collect(Collectors.toList());
        return in(column, collect);
    }

    public Split<Entity> notIn(Fn<Entity, Object> column, Collection<?> values) {
        checkColumn(column);
        if (values == null || values.isEmpty()) {
            return new Split<>(this);
        }
        if (values.stream().anyMatch(Objects::isNull)) {
            throw new IllegalArgumentException("values cannot contain null");
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), CollectionSqlExpression.NOT_IN, handleParamValue(values.toArray())));
    }

    public Split<Entity> notIn(Fn<Entity, Object> column, Object... values) {
        List<Object> collect = Arrays.stream(values).collect(Collectors.toList());
        return notIn(column, collect);
    }

    public Split<Entity> isNull(Fn<Entity, Object> column) {
        checkColumn(column);
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), NullSqlExpression.IS_NULL));
    }

    public Split<Entity> isNotNull(Fn<Entity, Object> column) {
        checkColumn(column);
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), NullSqlExpression.IS_NOT_NULL));
    }

    public Split<Entity> between(Fn<Entity, Object> column, Object startValue, Object endValue) {
        checkColumn(column);
        if (startValue == null && endValue == null) {
            throw new IllegalArgumentException("startValue and endValue cannot be null at the same time");
        }
        if (startValue == null || endValue == null) {
            return new Split<>(this);
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), ScopeSqlExpression.BETWEEN, handleParamValue(startValue, endValue)));
    }

    public Split<Entity> notBetween(Fn<Entity, Object> column, Object startValue, Object endValue) {
        checkColumn(column);
        if (startValue == null && endValue == null) {
            throw new IllegalArgumentException("startValue and endValue cannot be null at the same time");
        }
        if (startValue == null || endValue == null) {
            return new Split<>(this);
        }
        return new Split<>(this, new SingleConditionItem<>(popStack(), column.getName(), ScopeSqlExpression.NOT_BETWEEN, handleParamValue(startValue, endValue)));
    }

    private void checkColumn(Fn<Entity, Object> column) {
//        if (TableColumnUtils.checkColumn(tClass, column.getName())) {
//            throw ParamException.message("The column does not exist in the entity class");
//        }
    }

    private LogicSqlOperator popStack() {
        return this.andOrStack.empty() ? LogicSqlOperator.AND : this.andOrStack.pop();
    }

    public static class Split<Entity> {

        private final Where<Entity> where;

        public Split(Where<Entity> where) {
            this.where = where;
        }

        public Split(Where<Entity> where, SingleConditionItem<Entity> singleItem) {
            this.where = where;
            this.where.conditions.add(singleItem);
        }

        public Where<Entity> and() {
            this.where.andOrStack.add(LogicSqlOperator.AND);
            return this.where;
        }

        public Where<Entity> or() {
            this.where.andOrStack.add(LogicSqlOperator.OR);
            return this.where;
        }

        public Split<Entity> and(Consumer<Where<Entity>> consumer) {
            return common(LogicSqlOperator.AND, consumer);
        }

        public Split<Entity> or(Consumer<Where<Entity>> consumer) {
            return common(LogicSqlOperator.OR, consumer);
        }

        private Split<Entity> common(LogicSqlOperator andOr, Consumer<Where<Entity>> consumer) {
            Where<Entity> andWhere = new Where<>(this.where.entityClass, this.where.map, this.where.paramCount);
            consumer.accept(andWhere);
            int size = andWhere.conditions.size();
            if (size != 0) {
                if (size == 1) {
                    this.where.conditions.add(andWhere.conditions.get(0));
                } else {
                    this.where.conditions.add(new CombinationConditionItem(andOr, andWhere.conditions));
                }
            }
            return this;
        }

    }

    private ParamValue[] handleParamValue(Object... values) {
        ParamValue[] paramValues = new ParamValue[values.length];
        for (int i = 0; i < values.length; i++) {
            Object value = values[i];

            String paramName = CLMybatisConstants.PARAM_PREFIX + paramCount.incrementAndGet();

            String paramPath = CLMybatisConstants.PARAM_START +
                    CLMybatisConstants.WHERE_PARAM_NAME + "." + CLMybatisConstants.MAP + "." + paramName +
                    CLMybatisConstants.PARAM_END;

            map.put(paramName, value);
            paramValues[i] = new ParamValue(paramPath, value);
        }
        return paramValues;
    }

    public String getWhereSql() {
        if (this.conditions.isEmpty()) {
            return "";
        }
        String sql = this.conditions.stream().map(ConditionItem::getSql).collect(Collectors.joining(" "));
        // 删除第一个逻辑运算符
        String innerFirstLogicSqlOperator = this.conditions.get(0).getAndOr().getOperator();
        int i = sql.indexOf(innerFirstLogicSqlOperator);
        if (i != -1) {
            sql = sql.substring(i + innerFirstLogicSqlOperator.length());
        }
        return sql;
    }

}
